/*
 * File     : AbstractBufferStructureField.java
 * Created  : 20 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniTypes.STRUCTURE_HELPER_TYPE;
import static org.objectweb.asm.Opcodes.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.googlecode.dni.internal.DniTypes;
import com.googlecode.dni.internal.MemoryAccess;
import com.googlecode.dni.type.nativetype.NativeTypes;
import com.googlecode.dni.type.nativetype.SizeT;
import com.googlecode.dni.type.nativetype.Unsigned;


/**
 * Encapsulates details of a pointer to primitive structure's field used during
 * class generation.
 *
 * @author Matthew Wilson
 */
abstract class AbstractBufferStructureField extends SizedStructureField
{

    /** Primitive type for a pointer; assumes sizeof(size_t) == sizeof(void*) */
    static final PrimitiveType POINTER = PrimitiveType.getType( SizeT.class );

    /** These annotations make no sense. */
    private static final List< Class< ? extends Annotation > > FORBIDDED_ANNOTATION_TYPES =
        new ArrayList< Class< ? extends Annotation > >();

    /** Map of buffer types to primitive types. */
    static final Map< Class< ? extends Buffer >, Class< ? > > BUFFER_TO_PRIMITIVE_TYPES =
        new HashMap< Class< ? extends Buffer >, Class< ? > >();

    /** Map of buffer types to unit sizes. */
    private static final Map< Class< ? extends Buffer >, Integer > BUFFER_TO_UNIT_SIZES =
        new HashMap< Class< ? extends Buffer >, Integer >();

    static
    {
        FORBIDDED_ANNOTATION_TYPES.add( Unsigned.class );
        FORBIDDED_ANNOTATION_TYPES.addAll( NATIVE_SIZED_TYPES );

        BUFFER_TO_PRIMITIVE_TYPES.put( ByteBuffer.class,   byte.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( ShortBuffer.class,  short.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( CharBuffer.class,   char.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( IntBuffer.class,    int.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( LongBuffer.class,   long.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( FloatBuffer.class,  float.class );
        BUFFER_TO_PRIMITIVE_TYPES.put( DoubleBuffer.class, double.class );

        BUFFER_TO_UNIT_SIZES.put( ByteBuffer.class,   1 );
        BUFFER_TO_UNIT_SIZES.put( ShortBuffer.class,  2 );
        BUFFER_TO_UNIT_SIZES.put( CharBuffer.class,   2 );
        BUFFER_TO_UNIT_SIZES.put( IntBuffer.class,    4 );
        BUFFER_TO_UNIT_SIZES.put( LongBuffer.class,   8 );
        BUFFER_TO_UNIT_SIZES.put( FloatBuffer.class,  4 );
        BUFFER_TO_UNIT_SIZES.put( DoubleBuffer.class, 8 );
    }

    private Class< ? extends Buffer > bufferType;

    private PrimitiveType primitiveType;


    /**
     * {@inheritDoc}
     */
    @Override
    final Class< ? > getFieldType()
    {
        return this.bufferType;
    }

    /** {@inheritDoc} */
    @Override
    boolean allowsAdditionalSetPointer()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void setGetter( final Method getter )
    {
        super.setGetter( getter );

        this.bufferType = initBufferType( getter );

        checkAnnotations( FORBIDDED_ANNOTATION_TYPES );

        Class< ? > elementType = BUFFER_TO_PRIMITIVE_TYPES.get( this.bufferType );
        this.primitiveType = PrimitiveType.getType( elementType );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final void checkSetter( final Method method )
    {
        Class< ? >[] parameterTypes = method.getParameterTypes();
        if ( parameterTypes.length != 1 )
        {
            throw new IllegalArgumentException( "Structure setter must take one parameter: " + method );
        }

        assert this.bufferType != null;
        if ( !this.bufferType.equals( parameterTypes[ 0 ] ) )
        {
            throw new IllegalArgumentException( "Structure setter / getter types do not agree: " + method );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final String getCType()
    {
        return this.primitiveType.getCType() + "*";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final int getSize()
    {
        // size in the structure is that of a pointer
        return NativeTypes.POINTER_SIZE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final void writeSetter( final MethodVisitor methodVisitor,
                            final String implName )
    {
        writeGetPointer( methodVisitor );

        Label nonNull = new Label();
        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitJumpInsn( IFNONNULL, nonNull );

        methodVisitor.visitInsn( LCONST_0 );
        POINTER.writeSet( methodVisitor );
        methodVisitor.visitInsn( Opcodes.RETURN );

        methodVisitor.visitLabel( nonNull );

        // TODO v1.1 support check on naughty Buffer types (allocated from Java is dangerous)

        if ( isSized() )
        {
            writeCheckBufferSize( methodVisitor, implName );
        }

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       MemoryAccess.UNSAFE_ACCESS_TYPE,
                                       "getBufferPointer",
                                       "(Ljava/nio/Buffer;)J" );

        methodVisitor.visitVarInsn( ALOAD, 1 );
        methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                       "java/nio/Buffer",
                                       "position",
                                       "()I" );
        methodVisitor.visitInsn( I2L );
        methodVisitor.visitInsn( LADD );

        POINTER.writeSet( methodVisitor );
        methodVisitor.visitInsn( Opcodes.RETURN );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final void writeEqualsFragment( final MethodVisitor methodVisitor,
                                    final Label returnFalseLabel,
                                    final String implName )
    {
        // watch out; this is really just a pointer!
        // (signed comparison is fine; we're just testing equal values, so don't
        // care how they are promoted)

        // Could be very slightly more efficient on 32-bit by only comparing int
        // ... BUT I DON'T CARE

        writeGetPointer( methodVisitor );
        POINTER.writeGet( methodVisitor, false );

        methodVisitor.visitVarInsn( ALOAD, 2 );
        writeGetPointerFrom( methodVisitor );
        POINTER.writeGet( methodVisitor, false );

        methodVisitor.visitInsn( LCMP );
        methodVisitor.visitJumpInsn( IFNE, returnFalseLabel );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final void writeHashCodeFragment( final MethodVisitor methodVisitor, final String implName )
    {
        writeGetPointer( methodVisitor );
        POINTER.writeGet( methodVisitor, true );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       STRUCTURE_HELPER_TYPE,
                                       "hashCode",
                                       "(J)I" );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    final void writeToStringFragment( final MethodVisitor methodVisitor, final String implName )
    {
        methodVisitor.visitLdcInsn( "0x" );
        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );

        writeGetPointer( methodVisitor );
        POINTER.writeGet( methodVisitor, true );
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       "java/lang/Long",
                                       "toHexString",
                                       "(J)Ljava/lang/String;" );

        methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                       "java/lang/StringBuilder",
                                       "append",
                                       "(Ljava/lang/String;)Ljava/lang/StringBuilder;" );
    }

    /**
     * @return the buffer type
     */
    final Class< ? extends Buffer > getBufferType()
    {
        return this.bufferType;
    }

    /**
     * @return the buffer's unit size in bytes
     */
    final int getBufferUnitSize()
    {
        return BUFFER_TO_UNIT_SIZES.get( this.bufferType );
    }

    /**
     * @param getter
     *            the getter
     * @return the buffer type
     */
    abstract Class< ? extends Buffer > initBufferType( Method getter );

    private void writeCheckBufferSize( final MethodVisitor methodVisitor, final String implName )
    {
        methodVisitor.visitVarInsn( ALOAD, 1 );

        // unit size here is 1 because check(Dynamic|Fixed)BufferSize use
        // Buffer.remaining() to check the size, which works in the buffer's
        // unit size
        writeGetSize( methodVisitor, implName, 1 );

        String methodName = isDynamicallySized() ?  "checkDynamicBufferSize" : "checkFixedBufferSize";
        methodVisitor.visitMethodInsn( INVOKESTATIC,
                                       DniTypes.STRUCTURE_HELPER_TYPE,
                                       methodName,
                                       "(Ljava/nio/Buffer;I)V" );
    }

}
